package registry

import (
	"bufio"
	"fmt"
	"os"
	"runtime"
	"strings"

	"gopkg.openfuyao.cn/bkeadm/pkg/executor/docker"
	"gopkg.openfuyao.cn/bkeadm/pkg/global"
	"gopkg.openfuyao.cn/bkeadm/pkg/infrastructure"
	"gopkg.openfuyao.cn/bkeadm/utils"
	"gopkg.openfuyao.cn/bkeadm/utils/log"
)

var (
	needRemoveImage = []string{}
)

func (op *Options) MigrateImage() {
	if !infrastructure.IsDocker() {
		log.BKEFormat(log.ERROR, "This migrate instruction only supports running in docker environment.")
		return
	}

	imageList := []string{}
	if len(op.Image) > 0 {
		imageList = append(imageList, op.Image)
	}
	if len(op.File) > 0 {
		fi, err := os.Open(op.File)
		if err != nil {
			log.Error(err)
			return
		}
		buf := bufio.NewScanner(fi)
		for {
			if !buf.Scan() {
				break
			}
			if len(buf.Text()) == 0 {
				continue
			}
			imageList = append(imageList, buf.Text())
		}
	}
	if len(op.Arch) == 0 {
		op.Arch = runtime.GOARCH
	}
	archs := strings.Split(op.Arch, ",")
	for _, image := range imageList {
		sourceImage := op.Source + "/" + image
		sourceImage = strings.ReplaceAll(sourceImage, "//", "/")
		targetImage := op.Target + "/" + image
		targetImage = strings.ReplaceAll(targetImage, "//", "/")
		err := syncImage(sourceImage, targetImage, archs)
		if err != nil {
			log.Error(err)
			return
		}
	}
	log.BKEFormat(log.NIL, "Image migration completed. ")
	// cleanLocalImage
	cleanBuildImage()
}

// syncImage Synchronize an image between mirror repositories
// image collection of multiple architectures is supported in the following scenarios
// 1. The image itself is multi-architecture
// 2. The image is of a single architecture, and the suffix contains the -ARCH architecture
/*
multi-architecture image alpine:3.15
or
alpine:3.15-amd64
alpine:3.15-arm64
*/
func syncImage(source, target string, arch []string) error {
	var err error
	// Single architecture
	if len(arch) == 1 {
		imageAddress := source
		log.BKEFormat(log.NIL, fmt.Sprintf("Try pulling away the mirror image %s", imageAddress))
		if err = global.Docker.Pull(docker.ImageRef{
			Image:    imageAddress,
			Platform: arch[0],
		}, utils.RetryOptions{MaxRetry: 3, Delay: 1}); err != nil {
			log.BKEFormat(log.NIL, fmt.Sprintf("Failed to pull the mirror %s", err.Error()))
			imageAddress = source + "-" + arch[0]
			log.BKEFormat(log.NIL, fmt.Sprintf("Try pulling away the mirror image %s", imageAddress))
			if err = global.Docker.Pull(docker.ImageRef{
				Image:    imageAddress,
				Platform: arch[0],
			}, utils.RetryOptions{MaxRetry: 3, Delay: 1}); err != nil {
				log.BKEFormat(log.ERROR, fmt.Sprintf("Failed to pull the mirror %s", err.Error()))
				return err
			}
		}
		if err = global.Docker.Tag(imageAddress, target); err != nil {
			log.BKEFormat(log.ERROR, fmt.Sprintf("docker tag %s %s error: %v", imageAddress, target, err))
			return err
		}
		log.BKEFormat(log.NIL, fmt.Sprintf("docker push %s", target))
		push := fmt.Sprintf("docker push %s", target)
		if result, err := global.Command.ExecuteCommandWithOutput("/bin/sh", "-c", push); err != nil {
			log.BKEFormat(log.ERROR, fmt.Sprintf("Image push failed %s %s %s", target, result, err.Error()))
			return err
		}
		log.BKEFormat(log.NIL, fmt.Sprintf("docker push %s", target))
		needRemoveImage = append(needRemoveImage, imageAddress)
		needRemoveImage = append(needRemoveImage, target)
		return nil
	}
	// More than architecture
	manifestCreateCmd := fmt.Sprintf("docker manifest create --insecure %s", target)
	var manifestAnnotate []string
	for _, ar := range arch {
		imageAddress := source
		log.BKEFormat(log.NIL, fmt.Sprintf("Try pulling away the mirror image %s", imageAddress))
		if err = global.Docker.Pull(docker.ImageRef{
			Image:    imageAddress,
			Platform: ar,
		}, utils.RetryOptions{MaxRetry: 3, Delay: 1}); err != nil {
			log.BKEFormat(log.NIL, fmt.Sprintf("Failed to pull the mirror %s", err.Error()))
			imageAddress = source + "-" + ar
			log.BKEFormat(log.NIL, fmt.Sprintf("Try pulling away the mirror image %s", imageAddress))
			if err = global.Docker.Pull(docker.ImageRef{
				Image:    imageAddress,
				Platform: ar,
			}, utils.RetryOptions{MaxRetry: 3, Delay: 1}); err != nil {
				log.BKEFormat(log.ERROR, fmt.Sprintf("Failed to pull the mirror %s", err.Error()))
				return err
			}
		}
		targetArch := fmt.Sprintf("%s-%s", target, ar)
		if err = global.Docker.Tag(imageAddress, targetArch); err != nil {
			log.BKEFormat(log.ERROR, fmt.Sprintf("docker tag %s %s error: %v", imageAddress, target, err))
			return err
		}
		log.BKEFormat(log.NIL, fmt.Sprintf("docker tag %s %s", imageAddress, targetArch))
		if err = global.Docker.Remove(docker.ImageRef{Image: imageAddress}); err != nil {
			log.BKEFormat(log.ERROR, fmt.Sprintf("Image cannot be removed %s %s", imageAddress, err.Error()))
			return err
		}
		log.BKEFormat(log.NIL, fmt.Sprintf("docker push %s", targetArch))
		push := fmt.Sprintf("docker push %s", targetArch)
		if result, err := global.Command.ExecuteCommandWithOutput("/bin/sh", "-c", push); err != nil {
			log.BKEFormat(log.ERROR, fmt.Sprintf("Image push failed %s %s %s", targetArch, result, err.Error()))
			return err
		}

		needRemoveImage = append(needRemoveImage, targetArch)

		manifestCreateCmd += fmt.Sprintf("  --amend %s", targetArch)
		manifestAnnotate = append(manifestAnnotate, fmt.Sprintf("docker manifest annotate %s --os linux --arch %s %s", target, ar, targetArch))
	}
	// Integration architecture
	manifestEnv := os.Environ()
	manifestEnv = append(manifestEnv, "DOCKER_CLI_EXPERIMENTAL=enabled")
	if err = global.Command.ExecuteCommandWithEnv(manifestEnv, "/bin/sh", "-c", manifestCreateCmd); err != nil {
		log.BKEFormat(log.ERROR, fmt.Sprintf("%s, %v", manifestCreateCmd, err))
		return err
	}
	log.BKEFormat(log.NIL, manifestCreateCmd)
	for _, cmd := range manifestAnnotate {
		if err = global.Command.ExecuteCommandWithEnv(manifestEnv, "/bin/sh", "-c", cmd); err != nil {
			log.BKEFormat(log.ERROR, fmt.Sprintf("%s, %v", cmd, err))
			return err
		}
		log.BKEFormat(log.NIL, fmt.Sprintf("%s", cmd))
	}
	manifestPushCmd := fmt.Sprintf("docker manifest push --purge --insecure %s", target)
	if err = global.Command.ExecuteCommandWithEnv(manifestEnv, "/bin/sh", "-c", manifestPushCmd); err != nil {
		log.BKEFormat(log.ERROR, fmt.Sprintf("%s, %v", manifestPushCmd, err))
		return err
	}
	log.BKEFormat(log.NIL, fmt.Sprintf("%s", manifestPushCmd))
	return nil
}

func cleanBuildImage() {
	for _, image := range needRemoveImage {
		_ = global.Docker.Remove(docker.ImageRef{
			Image: image,
		})
	}
}
